release: v1.19.0#335
Merged
Merged
Conversation
Both from Samuel's hands-on testing of the sidebar shipped in v1.18.0. - sidebar.side (left|right, default right). Done with CSS `order` rather than by reordering the DOM, so content stays FIRST in source order: on mobile (single column) the reader gets the post before the sidebar either way, and only the desktop columns swap. - sidebar.blocks — one ORDERED CSV (about,recent,sections,connect) rather than toggles plus a separate order field. The list order IS the render order, and omitting a block hides it. That also settles #307 item 4: drop `sections` and your nav appears once instead of in both the header and the sidebar. New src/lib/sidebar.ts holds the pure helpers (parse/resolve/labels/guards), prisma-free so the admin client bundle can import it, mirroring @/lib/categories. Config plumbing follows the established four points: SITE_CONFIG_FIELDS, RuntimeSiteConfig, the overlay, and validateSiteConfigValue. The blocks validator REJECTS unknown names rather than dropping them, so a typo ("recentt") surfaces at save time instead of silently hiding a block. Sidebar.tsx renders from a keyed map in the configured order; each block still returns null when it has nothing to show. Admin controls appear only when the shell is "sidebar"; the wizard gains the side picker plus a review-step row. The blocks input uses the same raw-text state as categories, so typing a comma isn't eaten by a join()-bound controlled input. Verified live (LAYOUT_SHELL=sidebar): left renders grid-cols-[300px_1fr] with lg:order-1 and no [1fr_300px]; SIDEBAR_BLOCKS=connect,about hides Sections and puts Connect before About in the DOM. 600 tests pass. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…7, @atproto/api 0.20.31 (#320) Triage-verified patch refresh: framework patch (next 16.2.10→16.2.11) plus React, postcss, marked, @atproto/api and eslint-config-next. tsc clean, 600 tests, next build, audit unchanged (3 moderate — the known postcss-in-next chain, #12). sharp held at ^0.34.5 (duplicate-libvips risk, undetectable by CI); typescript still blocked upstream (#234). Confirmed single sharp @0.34.5, no nested copies. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…d + rotation-safe (#59) (#321) Push previously needed `npx web-push generate-vapid-keys` + three .env vars. Now Admin → Site settings → Phone notifications generates the keypair in one click. Mirrors the encrypted-secret pattern (analytics-secret.ts): public key + subject as plain SiteSetting rows under integration.push.*, the private signing key encrypted (secret-box), env fallback, status never exposes the private key. The hard part — key rotation: - Every generate/save/clear PURGES prisma.pushSubscription (deleteMany) because each subscription is bound to the OLD applicationServerKey and can never receive a send signed by a new key. - push.ts had module-level `const PUBLIC/PRIVATE` read once at import and a one-shot ensureConfigured() memoization — after a rotation it would keep signing with the dead key. Replaced with a fingerprint check: getVapidConfig() reads fresh, and setVapidDetails re-inits only when the active key changes. - PushSetup.tsx re-used a stale subscription while reporting "on" (no applicationServerKey comparison anywhere) → a zombie "on-but-dead" state. Now it compares the subscription's key to the server's current public key (and the server's subscription count) on init, and unsubscribes + re-subscribes on mismatch in enable(). New: src/lib/push-config.ts, /api/admin/push-keys route (generate/save/clear, verifyAdmin+verifyOrigin, never returns the private key), admin panel section with a rotation warning. push/route + push/test switched to the async getters. Tests (+17): encrypted round-trip + env fallback, status-never-leaks-private, purge-on-save/generate/clear, no-purge-without-ADMIN_SECRET, fingerprint re-init on rotation, route auth/CSRF/actions. 617 pass. On-device verification (rotation → re-subscribe, no zombie) is for the demo — covered by the PR's device checklist. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#322) Third built-in theme and the first to use the sidebar shell: a left sidebar of bio / recent posts / links beside a serif reading column, warm charcoal ground, pine-green accent. Distinct from the default (cool glass) and Editorial (sepia print). Dark, per the contrast invariant. Engine addition: a Theme can now preset the sidebar's side + blocks, not just the layout regions. New resolveSidebar(themeId, overrides) mirrors resolveLayout — owner override → theme preset → built-in default (right, all blocks) — and the site-settings sidebar resolution runs through it against the resolved theme id. RuntimeSiteConfig.sidebar stays the resolved shape, so (public)/layout.tsx and the admin are unchanged; existing sidebar tests pass untouched (the default theme presets nothing → same right/all-blocks). Classic Blog presets layout {shell:sidebar, header:minimal, feed:list} and sidebar {side:left, blocks:[about,recent,connect]} — sections omitted so the nav lives once (in the minimal header's menu), the combination flagged in #307. It auto-appears in the admin picker + wizard (both iterate THEMES) with zero UI wiring. Verified live (THEME=classic): left grid-cols-[300px_1fr] + <aside>, Sections absent, Connect present, and the pine/charcoal/serif tokens injected. Tests: resolveSidebar precedence, Classic Blog registration; the contrast invariant now covers it. 624 pass. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…59) (#323) The last two "safe + actionable" env-only settings from #59's audit. Both were read directly from process.env; now they overlay through the SiteSetting config like everything else, editable in Admin → Site settings → Security. - New "int" FieldType with a 0–3650 range check in validateSiteConfigValue. - security.adminSessionTtlDays (default 30) + security.appTokenTtlDays (0 = never expires) in SITE_CONFIG_FIELDS / RuntimeSiteConfig / defaults / overlay, with env (ADMIN_SESSION_TTL_DAYS / APP_TOKEN_TTL_DAYS) as the default via site.config. - auth.ts adminSessionTtlMs() and oauth/token appTokenExpiry() now read getRuntimeSiteConfig() (60s cache) — both via a dynamic import so the widely- imported auth module never statically pulls in site-settings' prisma graph. Both call sites were already async. Caveat surfaced in the UI: changes affect only newly-minted sessions/tokens; no purge, and setting an app-token TTL starts expiring newly-issued tokens. Tests: int validation (range/integer/negative), the TTL overlay, and the oauth-token test updated to drive the TTL through mocked runtime config rather than process.env. 626 pass. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
New installs can set a profile picture + banner during setup instead of only after, in the admin panel. /api/media needs an owner cookie that doesn't exist pre-setup, so this adds a setup-token-gated path. - Extracted the image optimise/EXIF-strip/write pipeline out of the media route into src/lib/media.ts (saveUploadedImage → returns the RELATIVE path, so a caller before SITE_URL is configured gets a stable value), plus a shared validateImagePath guard. The media route now calls the shared helper. - Hardened saveUploadedImage: a corrupt/undecodable image returns ok:false → a 400, not the previous unhandled 500 (benefits the media route too). - Extracted getOrCreateSetupToken into src/lib/setup-token.ts (+ verifySetupToken) so both /api/setup and the new upload agree on one token; the setup route now uses the shared helper. - New /api/setup/media, gated exactly like /api/setup: admin auth once ADMIN_SECRET is set, else the setup token (x-setup-token header). - Wizard: avatar/banner controls on the password step (where the token lives), posting to /api/setup/media, then avatarPath/bannerPath ride the /api/setup body and are validated + written into the claimed SiteSettings row. Verified live: wrong/no token → 401, valid token + real image → 201 + relative path, corrupt file → 400. Tests: validateImagePath, saveUploadedImage (unsupported + undecodable), the setup-media token gate, and the setup route's avatar/banner write + rejection. 639 pass. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…old (#250) (#325) docs/theming.md predated the whole theming engine — it told owners to hand-edit globals.css and swap avatar/banner files on disk (both now web-editable). Rewrote it around: (1) what's editable from Admin → Appearance (theme, per-region layout, accent, sidebar, avatar/banner); (2) the Theme token contract + buildThemeStyle diff + the region×variant model; (3) a developer scaffold for adding a built-in theme (one data file + one registry line), with the dark/contrast invariant, self-hosted-font limit, and pinned-accent-ramp constraints spelled out. This is the developer half of the title's "custom-theme scaffold". Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…py tree-wide (#329, #12) (#330) sharp <0.35.0 carries four libvips CVEs (GHSA-f88m-g3jw-g9cj, CVSS 7.0) and our direct dependency was the vulnerable one. Unlike the postcss chain this was reachable from remote input: proxyImage() in fedi-media.ts decodes bytes fetched from arbitrary remote instances, and /_next/image is a public endpoint that decodes thumbnails from the allow-listed PeerTube hosts. Bumping alone is not enough — Next pins its own optional sharp at ^0.34.5, so a disjoint direct range leaves a second vulnerable copy plus a duplicate libvips binary, which tsc/tests/build cannot detect. The overrides entry is what dedupes the tree onto one patched copy. npm rejects an override that conflicts with a direct dependency, so both use npm's $name reference and stay in sync on future bumps. The same treatment for postcss dedupes Next's nested copy and clears the last transitive advisory (#12): npm audit now reports 0 vulnerabilities. Verified beyond the standard gates, since a broken or duplicated native binary is invisible to them: npm ls shows one sharp (0.35.3 overridden, deduped under next), and a runtime round-trip exercising every sharp API the codebase uses (metadata, resize/fit-inside, rotate, webp, png, toBuffer, limitInputPixels) passes on libvips 8.18.3 — including confirming limitInputPixels still rejects an over-budget image, so the decompression-bomb defence is intact. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#331) Push has been unusable on any instance without VAPID keys since the admin generator shipped in #321, because the enrollment UI never checked for them. PushSetup only fetched /api/push when a subscription already existed, so with no keypair it never learned configured:false — it showed "Enable phone notifications" regardless, and enable() then threw "push not configured on server", rendered as small grey text. The panel that fixes it lives in Admin -> Site settings, and nothing pointed there. That is what a fresh install hit. Now the mount always asks the server, and a missing keypair is its own state: "Set up phone notifications" generates the keypair via /api/admin/push-keys and continues straight into the subscribe flow, so it stays one click. Safe from here — the bell renders only behind isAdmin in all three header variants, the route re-checks admin + origin, and with no keys there are no subscriptions for the generate to purge. Failures now surface the server's own reason (e.g. ADMIN_SECRET unset, so the private key can't be encrypted at rest). Notification.requestPermission() deliberately stays the first await in enable(), since the prompt must run inside the user gesture. The enrollment decision logic moves to src/lib/push-client.ts as pure functions over primitives, so it can be tested — the suite runs in the node environment with no DOM and the component is a client component. The rotation trap is locked down there too: a subscription bound to a rotated key, or any subscription when the server holds none, must resolve to "ready" and never "on". Also documents Web Push in docs/configuration.md (it had none anywhere) and corrects the stale layout.tsx comment claiming keys come from .env.local. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…) (#332) The blocker on a light theme — and on the in-admin custom palette that closes #250 — is that body text was hard-coded to Tailwind neutrals. `@theme` EXTENDS Tailwind's palette rather than replacing it, so `gray-*` stays fixed while `surface-*` moves: on a paper ground `text-white` lands at ~1:1, an invisible site. No theme token could reach those utilities. Adds a content-* text ramp (content / -strong / -muted / -subtle / -faint / -dim / -ghost, brightest to dimmest) as real theme tokens, and migrates the layout chrome onto it: 64 utilities across Navbar, the three header variants, the three footer variants, Sidebar, MobileMenu, NotificationBell and PushSetup. Zero visual change, by construction rather than by inspection: globals.css defaults each token to the Tailwind neutral it replaces (--color-content-faint: var(--color-gray-500)), so text-content-faint resolves through to the very same value — including the lab() refinement Tailwind emits for wide-gamut displays. All three themes share the default ramp, so every content token diffs out of buildThemeStyle and nothing extra is emitted; a test asserts that for editorial. Note the ramp's hexes are Tailwind v4's neutrals, not the v3 values people remember (gray-500 is #6a7282, not #6b7280) — v4 regenerated the palette in oklch. Using the remembered hexes would have shifted every migrated colour. The contrast invariant is now also expressed against the tokens — the form that survives the migration and that a light theme must satisfy — asserting the readable tier stays AA on each theme's own ground, plus ramp monotonicity. The hard-coded neutral check STAYS: most of the app is still un-migrated, so the dark-only guard has to hold until it isn't. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Also reorders the section headings to the documented contract (Added, Changed, Fixed, Security) before the section freezes — released sections are immutable afterwards, so this is the last point it can be corrected without a changelog-resync. Co-authored-by: Samuel Lison <samuel@slcolonies.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.19.0 — MINOR. Six features, two security fixes, no breaking changes.
Merges
devintomain. True merge, not a squash (shared history — seedocs/releasing.md).🔒 Security
/_next/image. Pinned tree-wide viaoverridesso only one patched copy exists.npm auditreports 0 vulnerabilities, first time since June.✨ Added
content-*text ramp (Theming system: admin-customizable layout + design (regions × variants presets), a Classic-blog second theme, and a custom-theme scaffold #250).🐛 Fixed
Verified
Full gate suite green on every constituent PR. Push confirmed end-to-end on the demo including key regeneration — the rotation path that invalidates every subscription.
sharpverified with a libvips runtime round-trip plus a live/_next/imagecheck on the demo.